/**
 * @file  ESPAsyncTCPbuffer.cpp
 * @date  22.01.2016
 * @author Markus Sattler
 *
 * Copyright (c) 2015 Markus Sattler. All rights reserved.
 * This file is part of the Asynv TCP for ESP.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 *
 */


#include <Arduino.h>
#include <debug.h>

#include "ESPAsyncTCPbuffer.h"


AsyncTCPbuffer::AsyncTCPbuffer(AsyncClient* client) {
    if(client == NULL) {
        DEBUG_ASYNC_TCP("[A-TCP] client is null!!!\n");
        panic();
    }

    _client = client;
    _TXbufferWrite = new cbuf(1460);
    _TXbufferRead = _TXbufferWrite;
    _RXbuffer = new cbuf(100);
    _RXmode = ATB_RX_MODE_FREE;
    _rxSize = 0;
    _rxTerminator = 0x00;
    _rxReadBytesPtr = NULL;
    _rxReadStringPtr = NULL;
    _cbDisconnect = NULL;

    _cbRX = NULL;
    _cbDone = NULL;
    _attachCallbacks();
}

AsyncTCPbuffer::~AsyncTCPbuffer() {
    if(_client) {
        _client->close();
    }

    if(_RXbuffer) {
        delete _RXbuffer;
        _RXbuffer = NULL;
    }

    if(_TXbufferWrite) {
        // will be deleted in _TXbufferRead chain
        _TXbufferWrite = NULL;
    }

    if(_TXbufferRead) {
        cbuf * next = _TXbufferRead->next;
        delete _TXbufferRead;
        while(next != NULL) {
            _TXbufferRead = next;
            next = _TXbufferRead->next;
            delete _TXbufferRead;
        }
        _TXbufferRead = NULL;
    }
}

size_t AsyncTCPbuffer::write(String & data) {
    return write(data.c_str(), data.length());
}

size_t AsyncTCPbuffer::write(uint8_t data) {
    return write(&data, 1);
}

size_t AsyncTCPbuffer::write(const char* data) {
    return write((const uint8_t *) data, strlen(data));
}

size_t AsyncTCPbuffer::write(const char *data, size_t len) {
    return write((const uint8_t *) data, len);
}

/**
 * write data in to buffer and try to send the data
 * @param data
 * @param len
 * @return
 */
size_t AsyncTCPbuffer::write(const uint8_t *data, size_t len) {
    if(_TXbufferWrite == NULL || _client == NULL || !_client->connected() || data == NULL || len == 0) {
        return 0;
    }

    size_t bytesLeft = len;
    while(bytesLeft) {
        size_t w = _TXbufferWrite->write((const char*) data, bytesLeft);
        bytesLeft -= w;
        data += w;
        _sendBuffer();

        // add new buffer since we have more data
        if(_TXbufferWrite->full() && bytesLeft > 0) {

            // to less ram!!!
            if(ESP.getFreeHeap() < 4096) {
                DEBUG_ASYNC_TCP("[A-TCP] run out of Heap can not send all Data!\n");
                return (len - bytesLeft);
            }

            cbuf * next = new cbuf(1460);

            if(next == NULL) {
                DEBUG_ASYNC_TCP("[A-TCP] run out of Heap!\n");
                panic();
            } else {
                DEBUG_ASYNC_TCP("[A-TCP] new cbuf\n");
            }

            // add new buffer to chain (current cbuf)
            _TXbufferWrite->next = next;

            // move ptr for next data
            _TXbufferWrite = next;
        }
    }

    return len;

}

/**
 * wait until all data has send out
 */
void AsyncTCPbuffer::flush() {
    while(!_TXbufferWrite->empty()) {
        while(!_client->canSend()) {
            delay(0);
        }
        _sendBuffer();
    }
}

void AsyncTCPbuffer::noCallback() {
    _RXmode = ATB_RX_MODE_NONE;
}

void AsyncTCPbuffer::readStringUntil(char terminator, String * str, AsyncTCPbufferDoneCb done) {
    if(_client == NULL) {
        return;
    }
    DEBUG_ASYNC_TCP("[A-TCP] readStringUntil terminator: %02X\n", terminator);
    _RXmode = ATB_RX_MODE_NONE;
    _cbDone = done;
    _rxReadStringPtr = str;
    _rxTerminator = terminator;
    _rxSize = 0;
    _RXmode = ATB_RX_MODE_TERMINATOR_STRING;
}

/*
 void AsyncTCPbuffer::readBytesUntil(char terminator, char *buffer, size_t length, AsyncTCPbufferDoneCb done) {
 _RXmode = ATB_RX_MODE_NONE;
 _cbDone = done;
 _rxReadBytesPtr = (uint8_t *) buffer;
 _rxTerminator = terminator;
 _rxSize = length;
 _RXmode = ATB_RX_MODE_TERMINATOR;
 _handleRxBuffer(NULL, 0);
 }

 void AsyncTCPbuffer::readBytesUntil(char terminator, uint8_t *buffer, size_t length, AsyncTCPbufferDoneCb done) {
 readBytesUntil(terminator, (char *) buffer, length, done);
 }
 */

void AsyncTCPbuffer::readBytes(char *buffer, size_t length, AsyncTCPbufferDoneCb done) {
    if(_client == NULL) {
        return;
    }
    DEBUG_ASYNC_TCP("[A-TCP] readBytes length: %d\n", length);
    _RXmode = ATB_RX_MODE_NONE;
    _cbDone = done;
    _rxReadBytesPtr = (uint8_t *) buffer;
    _rxSize = length;
    _RXmode = ATB_RX_MODE_READ_BYTES;
}

void AsyncTCPbuffer::readBytes(uint8_t *buffer, size_t length, AsyncTCPbufferDoneCb done) {
    readBytes((char *) buffer, length, done);
}

void AsyncTCPbuffer::onData(AsyncTCPbufferDataCb cb) {
    if(_client == NULL) {
        return;
    }
    DEBUG_ASYNC_TCP("[A-TCP] onData\n");
    _RXmode = ATB_RX_MODE_NONE;
    _cbDone = NULL;
    _cbRX = cb;
    _RXmode = ATB_RX_MODE_FREE;
}

void AsyncTCPbuffer::onDisconnect(AsyncTCPbufferDisconnectCb cb) {
    _cbDisconnect = cb;
}

IPAddress AsyncTCPbuffer::remoteIP() {
    if(!_client) {
        return IPAddress(0U);
    }
    return _client->remoteIP();
}

uint16_t AsyncTCPbuffer::remotePort() {
    if(!_client) {
        return 0;
    }
    return _client->remotePort();
}

bool AsyncTCPbuffer::connected() {
    if(!_client) {
        return false;
    }
    return _client->connected();
}

void AsyncTCPbuffer::stop() {

    if(!_client) {
        return;
    }
    _client->stop();
    _client = NULL;

    if(_cbDone) {
        switch(_RXmode) {
            case ATB_RX_MODE_READ_BYTES:
            case ATB_RX_MODE_TERMINATOR:
            case ATB_RX_MODE_TERMINATOR_STRING:
                _RXmode = ATB_RX_MODE_NONE;
                _cbDone(false, NULL);
                break;
            default:
              break;
        }
    }
    _RXmode = ATB_RX_MODE_NONE;
}

void AsyncTCPbuffer::close() {
    stop();
}


///--------------------------------

/**
 * attachCallbacks to AsyncClient class
 */
void AsyncTCPbuffer::_attachCallbacks() {
    if(!_client) {
        return;
    }
    DEBUG_ASYNC_TCP("[A-TCP] attachCallbacks\n");

    _client->onPoll([](void *obj, AsyncClient* c) {
        AsyncTCPbuffer* b = ((AsyncTCPbuffer*)(obj));
        if((b->_TXbufferRead != NULL) && !b->_TXbufferRead->empty()) {
            b->_sendBuffer();
        }
        //    if(!b->_RXbuffer->empty()) {
        //       b->_handleRxBuffer(NULL, 0);
        //   }
    }, this);

    _client->onAck([](void *obj, AsyncClient* c, size_t len, uint32_t time) {
        DEBUG_ASYNC_TCP("[A-TCP] onAck\n");
        ((AsyncTCPbuffer*)(obj))->_sendBuffer();
    }, this);

    _client->onDisconnect([](void *obj, AsyncClient* c) {
        DEBUG_ASYNC_TCP("[A-TCP] onDisconnect\n");
        AsyncTCPbuffer* b = ((AsyncTCPbuffer*)(obj));
        b->_client = NULL;
        bool del = true;
        if(b->_cbDisconnect) {
            del = b->_cbDisconnect(b);
        }
        delete c;
        if(del) {
            delete b;
        }
    }, this);

    _client->onData([](void *obj, AsyncClient* c, void *buf, size_t len) {
        AsyncTCPbuffer* b = ((AsyncTCPbuffer*)(obj));
        b->_rxData((uint8_t *)buf, len);
    }, this);

    _client->onTimeout([](void *obj, AsyncClient* c, uint32_t time){
        DEBUG_ASYNC_TCP("[A-TCP] onTimeout\n");
        c->close();
    }, this);

    DEBUG_ASYNC_TCP("[A-TCP] attachCallbacks Done.\n");
}

/**
 * send TX buffer if possible
 */
void AsyncTCPbuffer::_sendBuffer() {
    //DEBUG_ASYNC_TCP("[A-TCP] _sendBuffer...\n");
    size_t available = _TXbufferRead->available();
    if(available == 0 || _client == NULL || !_client->connected() || !_client->canSend()) {
        return;
    }

    while((_client->space() > 0) && (_TXbufferRead->available() > 0) && _client->canSend()) {

        available = _TXbufferRead->available();

        if(available > _client->space()) {
            available = _client->space();
        }

        char *out = new char[available];
        if(out == NULL) {
            DEBUG_ASYNC_TCP("[A-TCP] to less heap, try later.\n");
            return;
        }

        // read data from buffer
        _TXbufferRead->peek(out, available);

        // send data
        size_t send = _client->write((const char*) out, available);
        if(send != available) {
            DEBUG_ASYNC_TCP("[A-TCP] write failed send: %d available: %d \n", send, available);
        }

        // remove really send data from buffer
        _TXbufferRead->remove(send);

        // if buffer is empty and there is a other buffer in chain delete the empty one
        if(_TXbufferRead->available() == 0 && _TXbufferRead->next != NULL) {
            cbuf * old = _TXbufferRead;
            _TXbufferRead = _TXbufferRead->next;
            delete old;
            DEBUG_ASYNC_TCP("[A-TCP] delete cbuf\n");
        }

        delete out;
    }

}

/**
 * called on incoming data
 * @param buf
 * @param len
 */
void AsyncTCPbuffer::_rxData(uint8_t *buf, size_t len) {
    if(!_client || !_client->connected()) {
        DEBUG_ASYNC_TCP("[A-TCP] not connected!\n");
        return;
    }
    if(!_RXbuffer) {
        DEBUG_ASYNC_TCP("[A-TCP] _rxData no _RXbuffer!\n");
        return;
    }
    DEBUG_ASYNC_TCP("[A-TCP] _rxData len: %d RXmode: %d\n", len, _RXmode);

    size_t handled = 0;

    if(_RXmode != ATB_RX_MODE_NONE) {
        handled = _handleRxBuffer((uint8_t *) buf, len);
        buf += handled;
        len -= handled;

        // handle as much as possible before using the buffer
        if(_RXbuffer->empty()) {
            while(_RXmode != ATB_RX_MODE_NONE && handled != 0 && len > 0) {
                handled = _handleRxBuffer(buf, len);
                buf += handled;
                len -= handled;
            }
        }
    }

    if(len > 0) {

        if(_RXbuffer->room() < len) {
            // to less space
            DEBUG_ASYNC_TCP("[A-TCP] _rxData buffer full try resize\n");
            _RXbuffer->resizeAdd((len + _RXbuffer->room()));

            if(_RXbuffer->room() < len) {
                DEBUG_ASYNC_TCP("[A-TCP] _rxData buffer to full can only handle %d!!!\n", _RXbuffer->room());
            }
        }

        _RXbuffer->write((const char *) (buf), len);
    }

    if(!_RXbuffer->empty() && _RXmode != ATB_RX_MODE_NONE) {
        // handle as much as possible data in buffer
        handled = _handleRxBuffer(NULL, 0);
        while(_RXmode != ATB_RX_MODE_NONE && handled != 0) {
            handled = _handleRxBuffer(NULL, 0);
        }
    }

    // clean up ram
    if(_RXbuffer->empty() && _RXbuffer->room() != 100) {
        _RXbuffer->resize(100);
    }

}

/**
 *
 */
size_t AsyncTCPbuffer::_handleRxBuffer(uint8_t *buf, size_t len) {
    if(!_client || !_client->connected() || _RXbuffer == NULL) {
        return 0;
    }

    DEBUG_ASYNC_TCP("[A-TCP] _handleRxBuffer len: %d RXmode: %d\n", len, _RXmode);

    size_t BufferAvailable = _RXbuffer->available();
    size_t r = 0;

    if(_RXmode == ATB_RX_MODE_NONE) {
        return 0;
    } else if(_RXmode == ATB_RX_MODE_FREE) {
        if(_cbRX == NULL) {
            return 0;
        }

        if(BufferAvailable > 0) {
            uint8_t * b = new uint8_t[BufferAvailable];
            _RXbuffer->peek((char *) b, BufferAvailable);
            r = _cbRX(b, BufferAvailable);
            _RXbuffer->remove(r);
        }

        if(r == BufferAvailable && buf && (len > 0)) {
            return _cbRX(buf, len);
        } else {
            return 0;
        }

    } else if(_RXmode == ATB_RX_MODE_READ_BYTES) {
        if(_rxReadBytesPtr == NULL || _cbDone == NULL) {
            return 0;
        }

        size_t newReadCount = 0;

        if(BufferAvailable) {
            r = _RXbuffer->read((char *) _rxReadBytesPtr, _rxSize);
            _rxSize -= r;
            _rxReadBytesPtr += r;
        }

        if(_RXbuffer->empty() && (len > 0) && buf) {
            r = len;
            if(r > _rxSize) {
                r = _rxSize;
            }
            memcpy(_rxReadBytesPtr, buf, r);
            _rxReadBytesPtr += r;
            _rxSize -= r;
            newReadCount += r;
        }

        if(_rxSize == 0) {
            _RXmode = ATB_RX_MODE_NONE;
            _cbDone(true, NULL);
        }

        // add left over bytes to Buffer
        return newReadCount;

    } else if(_RXmode == ATB_RX_MODE_TERMINATOR) {
        // TODO implement read terminator non string

    } else if(_RXmode == ATB_RX_MODE_TERMINATOR_STRING) {
        if(_rxReadStringPtr == NULL || _cbDone == NULL) {
            return 0;
        }

        // handle Buffer
        if(BufferAvailable > 0) {
            while(!_RXbuffer->empty()) {
                char c = _RXbuffer->read();
                if(c == _rxTerminator || c == 0x00) {
                    _RXmode = ATB_RX_MODE_NONE;
                    _cbDone(true, _rxReadStringPtr);
                    return 0;
                } else {
                    (*_rxReadStringPtr) += c;
                }
            }
        }

        if(_RXbuffer->empty() && (len > 0) && buf) {
            size_t newReadCount = 0;
            while(newReadCount < len) {
                char c = (char) *buf;
                buf++;
                newReadCount++;
                if(c == _rxTerminator || c == 0x00) {
                    _RXmode = ATB_RX_MODE_NONE;
                    _cbDone(true, _rxReadStringPtr);
                    return newReadCount;
                } else {
                    (*_rxReadStringPtr) += c;
                }
            }
            return newReadCount;
        }
    }

    return 0;
}
